package se.lnu.axez;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;

import javax.microedition.khronos.opengles.GL11;

import se.lnu.axez.shaders.Shader;
import se.lnu.axez.shaders.ShaderProgram;
import android.opengl.GLES20;
import android.opengl.GLES20Fix;
import android.util.Log;

/**
 * Abstract triangle mesh object. Successor classes must implement methods
 * returning mesh vertices, normals and VU.
 * 
 * @author stan
 * 
 */
public abstract class TriMesh {
	/**
	 * Number of vertex coordinates.
	 */
	public static final int VERTEX_COORDINATES = Constants.VERTEX_COORDINATES;
	
	/**
	 * Number of VU coordinates.
	 */
	public static final int UV_COMPONENTS = Constants.UV_COMPONENTS;

	private static final String TAG = TriMesh.class.getName();
	private static final int INDICES_VBO = 0;
	private static final int VERTICES_VBO = 1;

	private boolean hasNormals;
	private boolean hasUV;
	private int vertexStructureSize;
	private int normalDataOffset;
	private int uvDataOffset;
	private final int[] vboIDs = new int[2];

	private int indexCount;

	public void draw(ShaderProgram shaderProgram) {
		this.bindHardwareBuffers();

		final int indexBufferName = this.getIndexBufferName();
		GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBufferName);
		Utils.checkGlError(TAG, "glBindBuffer");

		final int vertexDataBufferName = this.getVertexDataBufferName();
		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexDataBufferName);
		Utils.checkGlError(TAG, "glBindBuffer");

		if (shaderProgram.isFeatureRequested(Shader.SHADER_FEATURE_ATTR_POSITION)) {
			final int positionAttrID = shaderProgram.getAttributeID(Shader.ATTR_NAME_POSITION);
			GLES20.glEnableVertexAttribArray(positionAttrID);
			Utils.checkGlError(TAG, "glEnableVertexAttribArray positionAttrID");
			GLES20Fix.glVertexAttribPointer(positionAttrID,
					TriMesh.VERTEX_COORDINATES, GLES20.GL_FLOAT, false,
					this.vertexStructureSize, 0);
		}
		
		if (shaderProgram.isFeatureRequested(Shader.SHADER_FEATURE_ATTR_NORMAL)) {
			if (this.hasNormals) {
				final int normalAttrID = shaderProgram.getAttributeID(Shader.ATTR_NAME_NORMAL);

				GLES20.glEnableVertexAttribArray(normalAttrID);
				Utils.checkGlError(TAG, "glEnableVertexAttribArray normalAttrID");
				GLES20Fix.glVertexAttribPointer(normalAttrID,
						TriMesh.VERTEX_COORDINATES, GLES20.GL_FLOAT, false,
						this.vertexStructureSize, this.normalDataOffset);
			}
		}

		if (shaderProgram.isFeatureRequested(Shader.SHADER_FEATURE_ATTR_UV)) {
			if (this.hasUV) {
				final int textureAttrID = shaderProgram.getAttributeID(Shader.ATTR_NAME_UV);

				if (textureAttrID != ShaderProgram.INVALID_ID) {
					GLES20.glEnableVertexAttribArray(textureAttrID);
					Utils.checkGlError(TAG,
							"glEnableVertexAttribArray textureAttrID");
					GLES20Fix.glVertexAttribPointer(textureAttrID,
							TriMesh.VERTEX_COORDINATES, GLES20.GL_FLOAT, false,
							this.vertexStructureSize, this.uvDataOffset);
				}
			}
		}

		GLES20Fix.glDrawElements(GLES20.GL_TRIANGLES, this.indexCount, GLES20.GL_UNSIGNED_SHORT, 0);

		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
		GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
	}

	protected abstract float[] getVertices();
	protected abstract float[] getNormals();
	protected abstract float[] getUV();
	protected abstract int getIndexCount();

	private boolean bindHardwareBuffersRequired() {
		return this.vboIDs[INDICES_VBO] == 0;
	}

	private int getVertexDataBufferName() {
		return this.vboIDs[VERTICES_VBO];
	}

	private int getIndexBufferName() {
		return this.vboIDs[INDICES_VBO];
	}
	
	private void bindHardwareBuffers() {
		if (this.bindHardwareBuffersRequired() == false) {
			return;
		}

		Log.v(TAG, "bindHardwareBuffers");

		this.indexCount = this.getIndexCount();
		
		// Allocate and fill the index buffer.
		GLES20.glGenBuffers(2, this.vboIDs, 0);
		Utils.checkGlError(TAG, "glGenBuffers");

		GLES20.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, this.vboIDs[INDICES_VBO]);
		Utils.checkGlError(TAG, "glBindBuffer");

		final int indexBufferSize = this.indexCount * Buffers.CHAR_BYTES;
		CharBuffer indexBuffer = generateIndexBuffer(this.indexCount);
		GLES20.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBufferSize, indexBuffer, GLES20.GL_STATIC_DRAW);
		Utils.checkGlError(TAG, "glBufferData");

		// Unbind the element array buffer.
		GLES20.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
		Utils.checkGlError(TAG, "glBindBuffer");

		// Allocate and fill the vertex data buffer.
		ByteBuffer vertexDataBuffer = this.createVertexDataBuffer();
		GLES20.glBindBuffer(GL11.GL_ARRAY_BUFFER, this.vboIDs[VERTICES_VBO]);
		Utils.checkGlError(TAG, "glBindBuffer");

		final int vertexBufferSize = vertexDataBuffer.limit();
		GLES20.glBufferData(GL11.GL_ARRAY_BUFFER, vertexBufferSize, vertexDataBuffer, GL11.GL_STATIC_DRAW);
		Utils.checkGlError(TAG, "glBufferData");

		// Unbind the array buffer.
		GLES20.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
		Utils.checkGlError(TAG, "glBindBuffer");
	}
	
	private ByteBuffer createVertexDataBuffer() {
		// first validate data
		float[] verts = this.getVertices();
		if (verts == null) {
			throw new IllegalStateException("Vertex coordinates array is null.");
		}
		
		if (verts.length % VERTEX_COORDINATES != 0) {
			throw new IllegalStateException("Vertex coordinates array length must be a multiple of three.");
		}
		
		// calculate size of vertex structure
		this.vertexStructureSize = VERTEX_COORDINATES * Buffers.FLOAT_BYTES;

		float[] normals = this.getNormals();
		if (normals != null) {
			normalDataOffset = this.vertexStructureSize;
			this.vertexStructureSize += VERTEX_COORDINATES * Buffers.FLOAT_BYTES;
			this.hasNormals = true;
		} else {
			normalDataOffset = 0;
			this.hasNormals = false;
		}

		float[] vuCoords = getUV();
		if (vuCoords != null) {
			this.uvDataOffset = this.vertexStructureSize;
			this.vertexStructureSize += UV_COMPONENTS * Buffers.FLOAT_BYTES;
			this.hasUV = true;
		} else {
			this.uvDataOffset = 0;
			this.hasUV = false;
		}

		// allocate array of vertex structures
		final int vertexCount = verts.length / VERTEX_COORDINATES;
		ByteBuffer vertexDataBuffer = Buffers.allocateByte(this.vertexStructureSize * vertexCount);

		// fill array of vertex structures
		for (int i = 0; i < vertexCount; i++) {
			for (int j = 0; j < VERTEX_COORDINATES; j++) {
				vertexDataBuffer.putFloat(verts[i * VERTEX_COORDINATES + j]);
			}

			if (normals != null) {
				for (int j = 0; j < VERTEX_COORDINATES; j++) {
					vertexDataBuffer.putFloat(normals[i * VERTEX_COORDINATES + j]);
				}
			}

			if (vuCoords != null) {
				for (int j = 0; j < UV_COMPONENTS; j++) {
					vertexDataBuffer.putFloat(vuCoords[i * UV_COMPONENTS + j]);
				}
			}
		}

		vertexDataBuffer.rewind();

		return vertexDataBuffer;
	}
	
	void releaseHardwareBuffers() {
		Log.v(TAG, "bindHardwareBuffers");
		if (this.bindHardwareBuffersRequired() == false) {
			return;
		}

		GLES20.glDeleteBuffers(this.vboIDs.length, this.vboIDs, 0);
		this.vboIDs[VERTICES_VBO] = this.vboIDs[INDICES_VBO] = 0;
	}

	private static CharBuffer generateIndexBuffer(int indexCount) {
		CharBuffer buffer = Buffers.allocateChar(indexCount);
		for (int i = 0; i < indexCount; ++i) {
			buffer.put((char) i);
		}

		buffer.rewind();
		return buffer;
	}
}
